/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.primitives.cadmium;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import fr.x9c.cadmium.kernel.Block;
import fr.x9c.cadmium.kernel.CodeRunner;
import fr.x9c.cadmium.kernel.Fail;
import fr.x9c.cadmium.kernel.Primitive;
import fr.x9c.cadmium.kernel.PrimitiveProvider;
import fr.x9c.cadmium.kernel.Value;
import fr.x9c.cadmium.util.ByteCode;
import fr.x9c.cadmium.util.CustomClassLoader;

/**
 * This class provides primitives related to dynamic interface compilation.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.0
 * @since 1.0
 */
@PrimitiveProvider
public final class CreateInterface {

    /** Name prefix for unnamed class. */
    private static final String NAME_PREFIX =
        "fr.x9c.cadmium.primitives.cadmium.Interface";

    /** Identifier for unnamed class. */
    private static final AtomicInteger NEXT_ID = new AtomicInteger();

    /**
     * No instance of this class.
     */
    private CreateInterface() {
    } // end empty constructor

    /**
     * Creates an interface from passed methods. <br/>
     * The name of the interface is generated.
     * @param ctxt context
     * @param methods list of methods the interface should declare
     * @return a Java object representing the generated interface
     * @throws Fail.Exception if a reflection or code generation error occurs
     */
    @Primitive
    public static Value cadmium_create_interface(final CodeRunner ctxt,
                                                 final Value methods)
        throws Fail.Exception {
        try {
            final String name = CreateInterface.NAME_PREFIX
                + CreateInterface.NEXT_ID.incrementAndGet();
            return Cadmium.createObject(compile(name,
                                                decodeMethodList(methods)));
        } catch (final Exception e) {
            Cadmium.fail(ctxt, e);
            return null; // never reached
        } // end try/catch
    } // end method 'cadmium_create_interface(CodeRunner, Value)'

    /**
     * Creates an interface from passed methods.
     * @param ctxt context
     * @param methods list of methods the interface should declare
     * @param name name of the interface to create
     * @return a Java object representing the generated interface
     * @throws Fail.Exception if a reflection or code generation error occurs
     */
    @Primitive
    public static Value cadmium_create_named_interface(final CodeRunner ctxt,
                                                       final Value methods,
                                                       final Value name)
        throws Fail.Exception {
        try {
            return Cadmium.createObject(compile(name.asBlock().asString(),
                                                decodeMethodList(methods)));
        } catch (final Exception e) {
            Cadmium.fail(ctxt, e);
            return null; // never reached
        } // end try/catch
    } // end method 'cadmium_create_named_interface(CodeRunner, Value, Value)'

    /**
     * Converts a Caml list into a Java list of methods.
     * @param l Caml list to convert
     * @return converted list
     */
    private static List<Method> decodeMethodList(final Value l) {
        final List<Method> res = new LinkedList<Method>();
        Value list = l;
        while (list.isBlock()) {
            final Block listBlock = list.asBlock();
            final Block meth = listBlock.get(0).asBlock();
            res.add(new Method(meth.get(0).asBlock().asString(),
                               Cadmium.decodeTypeList(meth.get(1)),
                               Cadmium.decodeType(meth.get(2))));
            list = listBlock.get(1);
        } // end while
        return res;
    } // end method 'decodeMethodList(Value)'

    /**
     * Compiles (<i>i.e.</i> generated bytecode for) an interface.
     * @param name interface name (fully qualified name)
     * @param methods list of methods the interface should declare
     * @return the compiled and loaded class corresponding to the interface
     * @throws IOException if an error occurs during compilation
     */
    private static Class compile(final String name,
                                 final List<Method> methods)
        throws IOException {
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        final DataOutputStream out = new DataOutputStream(baos);

        if (methods.size() > 16000) {
            throw new IOException("Interface contains too many methods");
        } // end if

        // header
        out.writeInt(ByteCode.FILE_MAGIC);
        out.writeShort(ByteCode.MINOR_VERSION);
        out.writeShort(ByteCode.MAJOR_VERSION);

        // constant_pool count (actual_size + 1)
        out.writeShort(4 + (2 * methods.size()) + 1);

        // 1: this [CONSTANT_Class_info]
        out.writeByte(ByteCode.CONSTANT_CLASS);
        out.writeShort(2);

        // 2: class name [CONSTANT_Utf8_info]
        out.writeByte(ByteCode.CONSTANT_UTF8);
        out.writeUTF(name.replace(".", "/"));

        // 3: super [CONSTANT_Class_info]
        out.writeByte(ByteCode.CONSTANT_CLASS);
        out.writeShort(4);

        // 4: super class name [CONSTANT_Utf8_info]
        out.writeByte(ByteCode.CONSTANT_UTF8);
        out.writeUTF(Object.class.getName().replace(".", "/"));

        for (Method m : methods) {
            // name [CONSTANT_Utf8_info]
            out.writeByte(ByteCode.CONSTANT_UTF8);
            out.writeUTF(m.name);
            // descriptor [CONSTANT_Utf8_info]
            final StringBuilder desc = new StringBuilder();
            desc.append("(");
            for (Class c : m.params) {
                desc.append(descForType(c));
            } // end for
            desc.append(")");
            desc.append(descForType(m.ret));
            out.writeByte(ByteCode.CONSTANT_UTF8);
            out.writeUTF(desc.toString());
        } // end for

        // interface definition
        out.writeShort(ByteCode.ACC_PUBLIC
                       | ByteCode.ACC_INTERFACE
                       | ByteCode.ACC_ABSTRACT); // access_flags
        out.writeShort(1); // this_class
        out.writeShort(3); // super_class
        out.writeShort(0); // interfaces_count
        out.writeShort(0); // fields_count
        out.writeShort(methods.size()); // methods_count
        int i = 5;
        for (Method m : methods) {
            out.writeShort(ByteCode.ACC_PUBLIC
                           | ByteCode.ACC_ABSTRACT); // access_flags
            out.writeShort(i++); // name_index
            out.writeShort(i++); // descriptor_index
            out.writeShort(0); // attributes_count
        } // end for
        out.writeShort(0); // attributes_count (for class)
        return CustomClassLoader.INSTANCE.defineAndResolve(name,
                                                           baos.toByteArray());
    } // end method 'compile(String, List<Method>)'

    /**
     * Converts a Java type into its internal string representation.
     * @param c Java type to convert - should not be <tt>null</tt>
     * @return corresponding string in internal representation
     */
    private static String descForType(final Class c) {
        assert c != null : "null c";
        if (boolean.class.equals(c)) {
            return "Z";
        } else if (byte.class.equals(c)) {
            return "B";
        } else if (char.class.equals(c)) {
            return "C";
        } else if (double.class.equals(c)) {
            return "D";
        } else if (float.class.equals(c)) {
            return "F";
        } else if (int.class.equals(c)) {
            return "I";
        } else if (long.class.equals(c)) {
            return "J";
        } else if (short.class.equals(c)) {
            return "S";
        } else if (void.class.equals(c)) {
            return "V";
        } else if (c.isArray()) {
            return "[" + descForType(c.getComponentType());
        } else {
            return "L" + c.getCanonicalName().replace(".", "/") + ";";
        } // end if/elsif/else
    } // end method 'descForType(Class)'

    /**
     * Mere data structure representing a method with name as well as
     * parameter and return types. Classes from the <tt>java.lang.reflect</tt>
     * package cannot be used because we want to describe methods that do not
     * exist yet.
     */
    private static final class Method {

        /** Method name. */
        private final String name;

        /** Method parameter types. */
        private final Class[] params;

        /** Method return type. */
        private final Class ret;

        /**
         * Constructs a method.
         * @param n method name - should not be <tt>null</tt>
         * @param p method parameter types - should not be <tt>null</tt>
         * @param r method return type - should not be <tt>null</tt>
         */
        private Method(final String n, final Class[] p, final Class r) {
            assert n != null : "null n";
            assert p != null : "null p";
            assert r != null : "null r";
            this.name = n;
            this.params = p;
            this.ret = r;
        } // end constructor(String, Class[], Class)

    } // end inner-class 'Method'

} // end class 'CreateInterface'
